<?
/*  ================================================================== *\
    (C) Copyright 2010 by Kenneth Downs

    This file is part of Triangulum 

    Triangulum is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 3 of the License, or
    (at your option) any later version.

    Triangulum is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Triangulum; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor,
    Boston, MA  02110-1301  USA
    or visit http://www.gnu.org/licenses/gpl.html
\*  ================================================================== */
class triBuildPostgres extends triBuild {
    # ================================================================
    #
    #  PLATFORM-SPECIFIC PROPERTY OVERRIDES
    #
    # ================================================================
    protected $platform = 'postgres';
    

    # ================================================================
    #
    #  REWRITE SPECIFICATION OVERRIDES
    #
    # ================================================================
    /**
     * Postgres-specific column based SEQUENCE action
     * 
     * Converts type to serial
     * 
     * @param $table
     * @param $column
     * @param $cdetails
     * @return Boolean Always returns True
     */
    protected function rfRewriteTypeSerialPlatform($table,$column) { 
        $xdst = &$this->tables2[$table]['columns'][$column]['properties'];         
        # Set a default, change the type
        $sequence = 'sequence_'.$table.'_'.$column;
        $xdst['default'] = "nextval('$sequence')";
        $this->spec2Platform['sequences'][] = $sequence;
    }

    
    # ================================================================
    #
    #  PLATFORM-SPECIFIC GET-CURRENT STUFF
    #
    # ================================================================
    protected function getCurrentPlatform() {
        $ready=isset($this->arguments['db-host'])
            && isset($this->arguments['db-user'])
            && isset($this->arguments['db-passwd'])
            && isset($this->arguments['db-name']);
        if(!$ready) {
            $this->log("Not comparing current, not all parms present: db-host,"
                ." db-user, db-passwd, db-name");
            return;
        }
    
        # DO 2011-01-06 Lets add some magic...
        # Should be able to set this with arguments
        $connString = 
            'host=' .$this->arguments['db-host'] 
            .' user=' .$this->arguments['db-user'] 
            .' password=' .$this->arguments['db-passwd'] 
            .' dbname=' .$this->arguments['db-name'];
        $dbConn = pg_connect($connString);
        
        if(!$dbConn) {
            $this->error("CCON Failed attempting to connect to "
                ."database {$this->arguments['db-name']} "
                ."on host {$this->arguments['db-host']} "
                ."with username {$this->arguments['db-user']}"
                ,"CCON Fail connection to current db"
            );
            return;
        }
        $this->log("Connection OK, Getting description of database "
            .$this->arguments['db-name']." on ".$this->arguments['db-host']
        );
    
        # the first query is really about pulling columns,
        # the tables fall out of that 
        $sql="
select c.table_name,c.column_name
     , data_type
     , is_nullable
     , c.column_default
     , coalesce(c.character_maximum_length,c.numeric_precision) as precision
     , coalesce(c.numeric_scale)                                as scale
  from information_schema.tables t
  join information_schema.columns c ON t.table_name = c.table_name
 where t.table_schema = 'public'
   AND t.table_type   = 'BASE TABLE'
 order by c.table_name
        , c.column_name    ";
        $dbResults = pg_query($sql);
        $rows = pg_fetch_all($dbResults);
        
        # If no tables, we can return, there is nothing to do at all
        if(!$rows) return;
     
        foreach($rows as $row) {
            # Special handling for serial/sequence cols, blank out their
            # default value.  Also get rid of typecasts
            $default = $row['column_default'];
            $default = str_replace('::regclass','',$default);
            $default = str_replace('::bpchar'  ,'',$default);
            if(strpos($default,"'::")>0) {
                $default = substr($default,0,strpos($default,"'::")+1);
            }
            if(substr($default,0,4)<>'next') {
                $default = str_replace("'","",$default);
            }
            
            # Corrections for when we do not want precision/scale
            $noprec = array('integer');
            $noscale= array('integer');
            if(in_array($row['data_type'],$noprec))  $row['precision'] = '';
            if(in_array($row['data_type'],$noscale)) $row['scale'] = '';
            
            $this->curTables[$row['table_name']]['columns'][$row['column_name']] = array(
                'type'       => $row['data_type']
               ,'xprecision' => $row['precision']
               ,'xscale'     => $row['scale']
               ,'nullable'   => $row['is_nullable']
               ,'default'    => $default
            );
        }
        
        # Step 2, pull key definitions (this includes foreign keys)
        $sql="
select c.constraint_name
     , c.constraint_type
     , c.table_name
     , x.column_name
  from information_schema.table_constraints c
  JOIN information_schema.key_column_usage  x
    ON c.constraint_name = x.constraint_name
   AND c.table_schema    = x.table_schema
   AND c.table_name      = x.table_name
 where c.constraint_schema = 'public'
   AND c.constraint_type IN ('PRIMARY KEY','UNIQUE','FOREIGN KEY')
 order by c.constraint_name,x.ordinal_position";
        $dbResults = pg_query($sql);
        $rows = pg_fetch_all($dbResults);
        
        if($rows) {
            $keys = array();
            foreach($rows as $row) {
                # this keeps overwriting for every column.  no biggie
                $keys[$row['table_name']][$row['constraint_name']]['type']
                     = $row['constraint_type'];
                # add the columns
                $keys[$row['table_name']][$row['constraint_name']]['cols'][]
                     = $row['column_name'];
            }
            foreach($keys as $table=>$keys) {
                foreach($keys as $key=>$kdetails) {
                    $keylist = implode(',',$kdetails['cols']);
                    if($kdetails['type'] == 'FOREIGN KEY') { 
                        $this->curFkeys[$table][$keylist]['type'] = $kdetails['type'];
                        $this->curFkeys[$table][$keylist]['name'] = $key;
                    }
                    else {
                        $this->curKeys[$table][$keylist]['type'] = $kdetails['type'];
                        $this->curKeys[$table][$keylist]['name'] = $key;
                    }
                }
            }
        }

        # Step 3: Pull sequence definitions        
        $sql="select sequence_name from information_schema.sequences";
        $dbResults = pg_query($sql);
        $rows = pg_fetch_all($dbResults);
        if($rows) {
            foreach($rows as $row) {
                $this->curPlatform['sequences'][] = $row['sequence_name'];
            }
        }
    }

    # ================================================================
    #
    #  MAKEDIFF OVERRIDES
    #
    # ================================================================
    /**
     * Postgres-specific column based SEQUENCE action
     * 
     * Converts type to serial
     * 
     * @param $table
     * @param $column
     * @param $cdetails
     * @return Boolean Always returns True
     */
    protected function makeDiffPlatformPre() {
        $cSeqs = $this->arr($this->curPlatform  ,'sequences',array());
        $nSeqs = $this->arr($this->spec2Platform,'sequences',array());
        
        foreach($nSeqs as $sequence) {
            if(!in_Array($sequence,$cSeqs)) {
                $this->diffPlatform['sequences'][] = $sequence;   
            }
        }
    }

    # ================================================================
    #
    #  WRITE SCRIPT OVERRIDES
    #
    # ================================================================
    /**
     * Postgres-specific scripts to write
     * 
     */
    protected function writeScriptPlatform() {
        $sequences = $this->arr($this->diffPlatform,'sequences',array());
        foreach($sequences as $sequence) {
            $this->builds['sequences'][] = "CREATE SEQUENCE $sequence;\n";        
        }
    }


    # ================================================================
    #
    #  TRIGGER GENERATION METHOD OVERRIDES
    #
    # ================================================================
    /**
     * Postgres specific sproc finisher
     * 
     * @return String Returns function body for specific platform
     */
    private function loadSprocPlatform($name,$function) {
        return $function."\nLANGUAGE plpgsql;";
    }
    

    /**
     * Postgres-specific trigger based SEQUENCE action
     * 
     * Does nothing, we want no action for sequences
     * 
     * @param $table
     * @param $column
     * @param $cdetails
     * @return Boolean Always returns True
     */
    protected function triggerSequencePlatform($table,$column,$cdetails) {
        return true;
    }
    
    /**
     * Postgres specific timestamp code
     *
     * @param $table Table Name
     * @param $column Column Name
     * @param $cdetails Column Details
     * @param $auto Automation code
     * @return Boolean Always Returns True
     */
    protected function triggerTimestampPlatform($table,$column,$cdetails,$auto) {
        if($auto=='ts_ins' || $auto=='ts_ui') { 
            $trigger="new.$column = NOW();\n";
            $this->triggerFragment($table,'i','calcs',$trigger);
        }
        if($auto=='ts_upd' || $auto=='ts_ui') {            
            $trigger="new.$column = NOW();\n";
            $this->triggerFragment($table,'u','calcs',$trigger);
        }
    }   

    /**
     * Postgres-specific aggregation builder
     *
     */
    protected function triggerAggregationsPlatform(
        $table,$fkdetails,$sums,$counts,$mins,$maxes,$parent,$fkchd        
    ) {
        
        # This will be the list of changed values.  Every
        # column for every automation is included in this list.
        $a_old_new_vals_match = array();
        
        # These are the three lists of assignments for
        # insert, update, and delete for row-level triggers.
        $xi = array();
        $xu = array();
        $xd = array();

        $mi = array();
        $mu = array();
        $md = array();

        # The next automation is counts.  Notice we slip in 
        # a line for *ALL* platforms in the header table that
        # initializes these to zero.
        foreach($counts as $colchd=>$colpar) {
            # For count we do not track if the column changed
            #$a_old_new_vals_match[] = "@new_$colchd = @old_$colchd";

            $usl="{SET}@new_$colpar = 0;\n";
            $this->triggerTranslate($parent,'i','calcs',$usl);
            
            # Here are the row-level assignments
            $xi[] = "$colpar = $colpar + 1";
            $xd[] = "$colpar = $colpar - 1";

            # These are the statement level assignments
            $mi[] = "$colpar = $parent.$colpar + new.thecount";
            $md[] = "$colpar = $parent.$colpar - new.thecount";
            $mii[]= "COUNT(*) as thecount";
            $mdi[]= "COUNT(*) as thecount";
        }
        
        # The next automation is SUMS.  Like the counts above,
        # these also get the initial values of zero.
        foreach($sums as $colchd=>$colpar) {
            $a_old_new_vals_match[] = "new.$colchd = old.$colchd";

            # Row-level assignments
            $xi[] = "$colpar = $colpar + new.$colchd";
            $xu[] = "$colpar = $colpar + new.$colchd - old.$colchd";
            $xd[] = "$colpar = $colpar - old.$colchd";
            
            $this->tables2[$parent]['columns'][$colpar]['properties']['extra'] 
                = " DEFAULT 0";
        }

        # Here is MIN.  It is a royal pain.  In plain english this
        # is what we are doing:
        #
        #   ON an insert:
        #      -> if the new value is below a current min,
        #         update the parent, else do nothing
        #   ON a delete:
        #      -> if the deleted value was the min, recalc min
        #   ON an update:
        #      -> if new value is below current min, it is the new min
        #      -> else if old value was the min, recalc min
        #      -> else nothing
        #
        #   ...and of course we have the row-level and 
        #      statement-level versions of both sets of operations.
        #
        # NOTICE that MINS (and maxes) are NOT initialized to
        # zero.  They are NULL when there are no child rows.
        #
        $sqli = $this->fkSelfJoin($fkdetails,'','new.');
        $sqld = $this->fkSelfJoin($fkdetails,'','old.');
        foreach($mins as $colchd=>$colpar) {
            $a_old_new_vals_match[] = "@new_$colchd = @old_$colchd";

            $sqlmini="Select min($colchd) from $table where $sqli";
            $sqlmind="Select min($colchd) from $table where $sqld";
            $xi[]
                ="$colpar = CASE WHEN @new_$colchd < $parent.$colpar\n"
                ."                 OR $colpar IS NULL\n"
                ."               THEN @new_$colchd\n" 
                ."               ELSE $colpar END";
            $xd[]
                ="$colpar = CASE WHEN @old_$colchd > $colpar\n"
                ."               THEN $colpar\n"
                ."               ELSE ($sqlmind) END";
            $xu[]
                ="$colpar = CASE WHEN @new_$colchd < $colpar\n"
                ."               THEN @new_$colchd\n"
                ."               WHEN @old_$colchd > $colpar\n"
                ."               THEN $colpar\n"
                ."               ELSE ($sqlmini) END";
            
            $mi[]
                ="$colpar = CASE WHEN new.new_$colpar < $colpar\n"
                ."                 OR $colpar IS NULL\n"
                ."               THEN new.new_$colpar\n"
                ."               ELSE $colpar END";
            $mu[]
                ="$colpar = CASE WHEN new.new_$colpar < $colpar\n"
                ."               THEN new.new_$colpar\n"
                ."               WHEN new.old_$colpar > $colpar\n"
                ."               THEN $colpar\n"
                ."               ELSE ($sqlmini) END";
            $md[]
                ="$colpar = CASE WHEN new.old_$colpar > $colpar\n"
                ."               THEN $colpar\n"
                ."               ELSE ($sqlmini) END";
            $ins   = "MIN(inserted.$colchd) as new_$colpar";
            $del   = "MIN(deleted.$colchd) as old_$colpar";
            $mii[] = $ins;
            $mdi[] = $del;
            $mui[] = $ins;
            $mui[] = $del;
        }

        # Here is MAX.  It is the mirror image royal pain of MIN
        #
        $sqli = $this->fkSelfJoin($fkdetails,'','new.');
        $sqld = $this->fkSelfJoin($fkdetails,'','old.');
        foreach($maxes as $colchd=>$colpar) {
            $a_old_new_vals_match[] = "@new_$colchd = @old_$colchd";

            $sqlmaxi="Select max($colchd) from $table where $sqli";
            $sqlmaxd="Select max($colchd) from $table where $sqld";
            $xi[]
                ="$colpar = CASE WHEN @new_$colchd > $parent.$colpar\n"
                ."                 OR $colpar IS NULL\n"
                ."               THEN @new_$colchd\n" 
                ."               ELSE $colpar END";
            $xd[]
                ="$colpar = CASE WHEN @old_$colchd < $colpar\n"
                ."               THEN $colpar\n"
                ."               ELSE ($sqlmaxd) END";
            $xu[]
                ="$colpar = CASE WHEN @new_$colchd > $colpar\n"
                ."               THEN @new_$colchd\n"
                ."               WHEN @old_$colchd < $colpar\n"
                ."               THEN $colpar\n"
                ."               ELSE ($sqlmaxi) END";
            
            $mi[]
                ="$colpar = CASE WHEN new.new_$colpar > $colpar\n"
                ."                 OR $colpar IS NULL\n"
                ."               THEN new.new_$colpar\n"
                ."               ELSE $colpar END";
            $mu[]
                ="$colpar = CASE WHEN new.new_$colpar > $colpar\n"
                ."               THEN new.new_$colpar\n"
                ."               WHEN new.old_$colpar < $colpar\n"
                ."               THEN $colpar\n"
                ."               ELSE ($sqlmaxi) END";
            $md[]
                ="$colpar = CASE WHEN new.old_$colpar < $colpar\n"
                ."               THEN $colpar\n"
                ."               ELSE ($sqlmaxi) END";
            $ins   = "MAX(inserted.$colchd) as new_$colpar";
            $del   = "MAX(deleted.$colchd) as old_$colpar";
            $mii[] = $ins;
            $mdi[] = $del;
            $mui[] = $ins;
            $mui[] = $del;
        }

        
        # Collapse the three lists of assignments to the
        # comma-delimited lists
        $ilist = implode(",",$xi);
        $ulist = implode(",",$xu);
        $dlist = implode(",",$xd);
        
        # And the six lists for statement level stuff
        $ilistm= implode(",",$mi);
        $ulistm= implode(",",$mu);
        $dlistm= implode(",",$md);
        

        # We have completed the list of values for all
        # automations, collapse down the list used to test
        # for changed values.
        $old_new_vals_match = implode(" AND ",$a_old_new_vals_match);
        
        # Generate foreign key matching expressions to new and 
        # old values.
        $njoin = $this->joinParent($fkdetails,'new.',"$parent.");
        $djoin = $this->joinParent($fkdetails,'old.',"$parent.");
        
        # End result on the row-by-row platforms are three
        # expressions: the insert, the delete and update.
        $ins="UPDATE $parent\n"
            ."   SET $ilist\n"
            ." WHERE $njoin;\n";
        $del="UPDATE $parent\n"
            ."   SET $dlist\n"
            ." WHERE $djoin;\n";
        $upd="UPDATE $parent\n"
            ."   SET $ulist\n"
            ." WHERE $njoin;\n";
            
        $this->triggerFragment($table,'i','aggpush',$ins);
        $this->triggerFragment($table,'u','aggpush',$upd);
        $this->triggerFragment($table,'d','aggpush',$del);
        return;
        # <------------ EARLY RETURN
        #               The rest of this code is "dead" but I'm
        #               keeping it here because we need to reinstate
        #               a more sophisticated case for handling
        #               changes to the foreign key
        
        # Now that we have the expressions, we can do the
        # two easy triggers, which are insert and delete
        /*
        if(count($xi)>0) {
            $this->triggerTranslate($table,'i','aggpush',$ins,'!mssql');
        }
        if(count($xd)>0) {
            $this->triggerTranslate($table,'d','aggpush',$del,'!mssql');
        }
        */

        
        # The more complicated case is update, where we check
        # for matching fks and changed values before doing anything
        # Here is what is going on:
        #
        #   The first part of $trg is a conditional that sees if
        #   the foreign key value changed.  If it did, it is kind
        #   of simple, it does a delete from the old value of the
        #   fk, and an insert to the new value.
        #
        #   If there are no update expressions (which happens if
        #   the user only has a COUNT), we stick on an endif.  But
        #   if there are update expressions, we stick on another
        #   conditional that only updates the parents if one of the
        #   child values has actually changed.  If nothing has 
        #   changed there is no reason to update the parent.
        $old_new_fk_match = $this->fkSelfJoin($fkdetails,'@new_','@old_');
        $trg="{IF} NOT($old_new_fk_match) {THEN}\n"
            .$this->indent($ins,4)."\n"
            .$this->indent($del,4)."\n";
        if(count($xu)==0) {
            $trg.="{ENDIF}\n";
        }
        else {
            $trg.="{ELSE}\n"
                ."    {IF} NOT ($old_new_vals_match) {THEN}\n"
                .$this->indent($upd,8)."\n"
                ."    {ENDIF}\n"
                ."{ENDIF}\n";
        }
        $this->triggerTranslate($table,'u','aggpush',$trg,'!mssql');
        
        # Once again, update is complicated, but not all that 
        # complicated.  We are doing three separate statements,
        # one for no fk change, and two for fk change (ins and del).

        # First is normal update, which occurs only if there are
        # update actions to perform.
        $old_new_fk_match=$this->fkSelfJoin($fkdetails,'inserted.','deleted.');
        $old_new_pk_match=$this->SelfJoin($table,'inserted.','deleted.');
        $old_new_vals_match = str_replace(
            '@new_','inserted.',$old_new_vals_match
        );
        $old_new_vals_match = str_replace(
            '@old_','deleted.',$old_new_vals_match
        );
        $fkchdu = 'inserted.'
            .implode("\n,inserted.",$fkdetails['properties']['colschd']);
        if($old_new_vals_match<>'') {
            $upd="-- Normal update push\n"
                ."UPDATE $parent\n"
                ."   SET $ulistm\n" 
                ."  FROM (SELECT $fkchdu\n"
                ."              ,$ulisti\n"
                ."          FROM inserted JOIN deleted\n"
                ."            ON $old_new_fk_match\n"
                ."           AND $old_new_pk_match\n"
                ."         WHERE NOT ($old_new_vals_match)\n"
                ."         GROUP BY $fkchdu\n"
                ."        ) new\n"
                ." WHERE $njoin;\n";
            $this->triggerFragment($table,'u','aggpush',$upd);
        }

        $trg="-- Insert Push when fk changes\n"
            ."UPDATE $parent\n"
            ."   SET $ilistm\n" 
            ."  FROM (SELECT $fkchd\n"
            ."              ,$ilisti\n"
            ."          FROM inserted\n"
            ."         WHERE NOT EXISTS (\n"
            ."            Select * from DELETED\n"
            ."             where $old_new_fk_match\n"
            ."               AND $old_new_pk_match)\n"
            ."         GROUP BY $fkchd\n"
            ."        ) new\n"
            ." WHERE $njoin;\n";
        $this->triggerFragment($table,'u','aggpush',$trg);
        $trg="-- Delete Push when fk changes\n"
            ."UPDATE $parent\n"
            ."   SET $dlistm\n" 
            ."  FROM (SELECT $fkchd\n"
            ."              ,$dlisti\n"
            ."          FROM deleted\n"
            ."         WHERE NOT EXISTS (\n"
            ."            Select * FROM inserted\n"
            ."             where $old_new_fk_match\n"
            ."               AND $old_new_pk_match)\n"
            ."         GROUP BY $fkchd\n"
            ."        ) new\n"
            ." WHERE $njoin;\n";
        $this->triggerFragment($table,'u','aggpush',$trg);
        
        return;
    } 
    
    protected function triggerFetchesPlatform(
        $table,$fkdetails,$fetches,$parent,$fkchd,$comment
    ) {

        # Make the list of columns and collapse them down
        $akidcols = array();
        $amssql = array();
        $mysqldec = $mysqlset = $mysqlinto = array();
        foreach($fetches as $parcol=>$kidcol) {
            $mysqldec[]="DECLARE fetch_$kidcol "
                .$this->columnProp($table,$kidcol,'type').";";
            $mysqlset[]= "    {MYSQL}SET new.$kidcol = fetch_$kidcol;";
            $amssql[] = "new.$kidcol = $parcol";
            $akidcols[] = "new.$kidcol";
            $mysqlinto[] = "fetch_$kidcol";
        }
        $mssql     = implode(",",$amssql);
        $kidcols   = implode(",",$akidcols);
        $parcols   = implode(",",array_keys($fetches));
        $mysqldec  = implode("\n",$mysqldec);
        $mysqlset  = implode("\n",$mysqlset);
        $mysqlinto = implode(",", $mysqlinto);
        
        # Make the list of not-null or changed fk values
        $afk_changed = array();
        $afk_notnull = array();
        foreach($fkdetails['properties']['colschd'] as $col) {
            $afk_changed[] = "new.$col <> old.$col";
            $afk_notnull[] = "new.$col IS NOT NULL";
        }
        $fk_changed = implode(",",$afk_changed);
        $fk_notnull = implode(",",$afk_notnull);
        
        # Finally the fk join
        $fkjoin = $this->joinParent($fkdetails,'new.',"$parent.");
        
        # case postgres
        $trigger
            ="IF $fk_notnull THEN\n"
            ."    SELECT INTO $kidcols\n"
            ."                $parcols\n"
            ."      FROM $parent\n"
            ."     WHERE $fkjoin;\n"
            ."END IF;\n";
        $this->triggerFragment($table,'i','calcs',$trigger);
        $trigger
            ="IF $fk_changed THEN\n"
            ."    SELECT INTO $kidcols\n"
            ."                $parcols\n"
            ."      FROM $parent\n"
            ."     WHERE $fkjoin;\n"
            ."END IF;\n";
        $this->triggerFragment($table,'u','calcs',$trigger);
        
        return true;   
    }
    
    
    # ================================================================
    #
    #  GLOBAL TRIGGER UTITLITY OVERRIDES
    #
    # ================================================================
    /**
     * Postgres specific code translator
     *
     * NOTUSED?  This may not be necessary if we code all
     *           triggers platform-specific
     * 
     * @return String Returns code translated for Postgres
     */
    protected function codeTranslate($code) {
        echo "\nHere we are in postgres code translate\n";
        # Here are the hardcoded translations
        $txlate=array(
            '{IF}'    =>'IF'
            ,'{THEN}' =>'THEN'
            ,'{ELSE}' =>'ELSE'
            ,'{ENDIF}'=>'END IF;'
            ,'{SET}'  =>''
            ,'{TYPEINT}'=>'int'
        );
        
        # First thing is do straight string substitutions
        # on all code
        foreach($txlate as $search=>$replace) {
            $code = str_replace($search,$replace,$code);
        }
        
        # Next straight string translation is "!" into "@new_"
        $code = str_replace("!","@new_",$code); 
        
        
        # Now explode into lines, and remove lines that
        # are marked as not for this platform, or remove
        # the marker if they are for this platform.
        # Also set up errors
        $marker = "{".$this->platform."}";
        $marlen = strlen($marker);
        $lines = explode("\n",$code);
        foreach($lines as $idx=>$line) {
            $line = trim($line);
            
            if(substr(strtolower($line),0,7)=='{error:') {
                $line = "{TRI:".substr($line,7);
                $lines[$idx] = '    '.$this->genError($platform,$line);
                continue;
            }
            
            if(substr($line,0,1)=='{') {
                if(substr(strtolower($line),0,$marlen)!=$marker) {
                    unset($lines[$idx]);
                }
                else {
                    $lines[$idx] = preg_replace(
                        "/\{$this->platform\}/i",'',$lines[$idx]
                    );   
                }
            }
        }
        $code = implode("\n",$lines);
        
        $code = str_replace("@new_","new.",$code);
        $code = str_replace("@old_","old.",$code);
        
        return $code.";\n";
    
    }

    

    
    /**
     * Postgres specific trigger finisher
     * 
     * @return Boolean always returns true
     */
    protected function finishTrigger($table,$uid,$tbody,$ba) {
        $uidx = $uid;
        if($uid=='i') $uid = 'insert';
        if($uid=='u') $uid = 'update';
        if($uid=='d') $uid = 'delete';
        
        # Pull out the declares
        $declares 
            = isset($this->declares[$table][$uidx])
            ? implode("\n",$this->declares[$table][$uidx])."\n"
            : '';
        $declares = $this->indent($declares,4);
        

        # Make up the trigger name
        $tf    = "{$table}_{$uid}_$ba";

        # Getting to work, let's do all pretty-fying of
        # trigger code.  Step 1 is to indent comma-lists
        $lines = explode("\n",$tbody);
        $tbody = '';
        foreach($lines as $line) {
            if(strpos($line,",")===false) {
                $tbody.="$line\n";
            }
            else {
                # Split and take care of the first line
                $commas = explode(",",$line);
                $tbody.=$commas[0]."\n";
                
                # Now indent the rest of the lines
                $xspace = strrchr($commas[0],' ');
                $xatvar = strrchr($commas[0],'@'); # mssql select into...
                if($xatvar!==false) {
                    $indent = strpos($commas[0],$xatvar)-1;
                }
                else {
                    $indent = strpos($commas[0],$xspace);
                }
                for($x=1;$x<=count($commas)-1;$x++) {
                    $tbody.=str_repeat(' ',$indent).",".$commas[$x]."\n";
                }
            }
        }
        
        
        # Final pretty step is to indent
        $tbody    = $this->indent($tbody,4);
        
        # Now the long-running annoying part, assembling for
        # the various flavors of SQL
        $return = 'new';
        if($uid=='delete') $return = 'old';
        # step 1, assemble the function
        $tbody 
            ="\n\n"
            ."-- --------------------------------------------- --\n"
            ."CREATE OR REPLACE FUNCTION {$tf}_F()\n"
            ."RETURNS TRIGGER\n AS \$BODY\$\n"
            ."DECLARE\n"
            .$declares
            ."BEGIN\n"
            ."    SET SEARCH_PATH TO PUBLIC;\n"
            .$tbody."\n"
            ."    RETURN $return;\n"
            ."END; \$BODY\$\n"
            ."LANGUAGE 'plpgsql' SECURITY DEFINER;\n\n"
            ."ALTER FUNCTION {$tf}_F() OWNER TO postgres;\n\n"
            ."CREATE TRIGGER {$tf}_T\n"
            ."   $ba $uid\n"
            ."   ON $table\n"
            ."   FOR EACH ROW\n"
            ."   EXECUTE PROCEDURE {$tf}_F();\n\n";

        # At the very end, add to script
        $this->builds['triggers'][] = $tbody;
        
        return true;
    }
    
}

